﻿// -----------------------------------------------------------------------
// <copyright file="AzureStorageContainer.cs">
// Copyright (c) 2013-2014 Andrey Veselov. All rights reserved.
// License:  Microsoft Public License (MS-PL)
// Contacts: http://andrey.moveax.com  andrey@moveax.com
// </copyright>
// -----------------------------------------------------------------------

namespace FileWarehouse.Azure
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.IO;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.WindowsAzure.Storage.Blob;

    public class AzureStorageContainer : IStorageContainer
    {
        private readonly CloudBlobContainer _blobContainer;

        private static readonly Task _successTask;

        static AzureStorageContainer()
        {
            var tcs = new TaskCompletionSource<bool>();
            tcs.SetResult(true);

            AzureStorageContainer._successTask = tcs.Task;
        }

        public AzureStorageContainer(CloudBlobContainer blobContainer)
        {
            Contract.Requires(blobContainer != null);

            this._blobContainer = blobContainer;
        }

        #region Permissions

        public async Task<ContainerPermission> GetPermissionAsync()
        {
            BlobContainerPermissions permissions;

            try {
                permissions = await this._blobContainer.GetPermissionsAsync().ConfigureAwait(false);
            }
            catch (Exception e) {
                throw new StorageException(StorageExceptionReason.StorageOperationFailed, "Unable to get permissions for the storage.", e);
            }

            return permissions.PublicAccess == BlobContainerPublicAccessType.Blob ?
                ContainerPermission.Public :
                ContainerPermission.Private;
        }

        public async Task SetPermissionAsync(ContainerPermission permission)
        {
            BlobContainerPublicAccessType accessType = permission.HasFlag(ContainerPermission.Public) ?
                BlobContainerPublicAccessType.Blob :
                BlobContainerPublicAccessType.Off;

            try {
                await this._blobContainer
                    .SetPermissionsAsync(new BlobContainerPermissions { PublicAccess = accessType })
                    .ConfigureAwait(false);
            }
            catch (Exception e) {
                throw new StorageException(StorageExceptionReason.StorageOperationFailed, "Unable to set permissions for the storage.", e);
            }
        }

        #endregion Permissions

        #region Directories methods

        public Task CreateDirectoryAsync(string path)
        {
            // nothing to do for Azure Blob Storage
            return AzureStorageContainer._successTask;
        }

        public async Task DeleteDirectoryAsync(string path)
        {
            CloudBlobDirectory blobDirectory =
                this._blobContainer.GetDirectoryReference(this.GetCorrectPath(path, true));

            try {
                var blobs = blobDirectory.ListBlobs(useFlatBlobListing: true);

                foreach (var blob in blobs) {
                    if (blob is CloudPageBlob)
                        await ((CloudPageBlob)blob).DeleteAsync().ConfigureAwait(false);
                    else if (blob is CloudBlockBlob)
                        await ((CloudBlockBlob)blob).DeleteAsync().ConfigureAwait(false);
                }
            }
            catch (Exception e) {
                throw new StorageException(
                    StorageExceptionReason.StorageOperationFailed,
                    string.Format("Unable to delete the directory {0} from the storage.", path),
                    e);
            }
        }

        public Task<IEnumerable<string>> GetDirectoriesAsync(string path)
        {
            var tcs = new TaskCompletionSource<IEnumerable<string>>();

            Task.Run(() => {
                try {
                    var items = this._blobContainer
                        .ListBlobs(this.GetCorrectPath(path, true), useFlatBlobListing: false)
                        .Where(item => item is CloudBlobDirectory)
                        .Select(item => item.Uri.Segments.Last().TrimEnd(new[] { '/', '\\' }))
                        .ToList();

                    tcs.SetResult(items);
                }
                catch (Exception e) {
                    tcs.SetException(
                        new StorageException(
                            StorageExceptionReason.StorageOperationFailed,
                            string.Format("Unable to get the directories list. {0}", path),
                            e));
                }
            });

            return tcs.Task;
        }

        #endregion Directories methods

        #region File operations

        public async Task WriteFileAsync(Stream source, string path)
        {
            CloudBlockBlob blockBlob = this._blobContainer.GetBlockBlobReference(this.GetCorrectPath(path, false));

            try {
                await blockBlob.UploadFromStreamAsync(source).ConfigureAwait(false);
            }
            catch (Exception e) {
                throw new StorageException(
                    StorageExceptionReason.StorageOperationFailed,
                    string.Format("Error while writing the file. {0}", path),
                    e);
            }
        }

        public async Task<Stream> GetFileStreamAsync(string path)
        {
            CloudBlockBlob blockBlob = this._blobContainer.GetBlockBlobReference(this.GetCorrectPath(path, false));

            var ms = new MemoryStream();
            try {
                await blockBlob.DownloadToStreamAsync(ms).ConfigureAwait(false);
                ms.Position = 0;
            }
            catch (Exception e) {
                throw new StorageException(
                    StorageExceptionReason.StorageOperationFailed,
                    string.Format("Error while reading the file. {0}", path),
                    e);
            }

            return ms;
        }

        public async Task GetFileStreamAsync(Stream target, string path)
        {
            CloudBlockBlob blockBlob = this._blobContainer.GetBlockBlobReference(this.GetCorrectPath(path, false));

            try {
                var pos = target.Position;
                await blockBlob.DownloadToStreamAsync(target).ConfigureAwait(false);
                target.Position = pos;
            }
            catch (Exception e) {
                throw new StorageException(
                    StorageExceptionReason.StorageOperationFailed,
                    string.Format("Error while reading the file. {0}", path),
                    e);
            }
        }

        public async Task DeleteFileAsync(string path)
        {
            CloudBlockBlob blockBlob = this._blobContainer.GetBlockBlobReference(this.GetCorrectPath(path, false));

            try {
                await blockBlob.DeleteIfExistsAsync().ConfigureAwait(false);
            }
            catch (Exception e) {
                throw new StorageException(
                    StorageExceptionReason.StorageOperationFailed,
                    string.Format("Error while deleting the file. {0}", path),
                    e);
            }
        }

        public Task<IEnumerable<string>> GetFilesAsync(string path, StorageSearchOption searchOption)
        {
            var tcs = new TaskCompletionSource<IEnumerable<string>>();

            Task.Run(() => {
                try {
                    // ReSharper disable RedundantCast
                    var selector = searchOption.HasFlag(StorageSearchOption.StripPaths) ?
                        (Func<IListBlobItem, string>)(item => item.Uri.Segments.Last()) :
                        (Func<IListBlobItem, string>)(item => {
                            string url = item.Uri.ToString();
                            var containerUrlLenght = this._blobContainer.Uri.ToString().Length;
                            if (containerUrlLenght < url.Length)
                                url = url.Substring(containerUrlLenght + 1);

                            return url;
                        });

                    // ReSharper restore RedundantCast

                    IEnumerable<string> files = this._blobContainer
                            .ListBlobs(this.GetCorrectPath(path, true), searchOption.HasFlag(StorageSearchOption.AllDirectories))
                            .Where(item => !(item is CloudBlobDirectory))
                            .Select(selector)
                            .ToList();

                    tcs.SetResult(files);
                }
                catch (Exception e) {
                    tcs.SetException(
                        new StorageException(
                            StorageExceptionReason.StorageOperationFailed,
                            string.Format("Unable to get the files list. {0}", path),
                            e));
                }
            });

            return tcs.Task;
        }

        public string GetUrlForFile(string path)
        {
            CloudBlockBlob blockBlob = this._blobContainer.GetBlockBlobReference(this.GetCorrectPath(path, false));

            try {
                return blockBlob.Uri.ToString();
            }
            catch (Exception e) {
                throw new StorageException(
                    StorageExceptionReason.StorageOperationFailed,
                    string.Format("Error while getting the file url. {0}", path),
                    e);
            }
        }

        #endregion File operations

        private string GetCorrectPath(string source, bool isDirectory)
        {
            if (string.IsNullOrEmpty(source))
                return source;

            var path = source.Replace('\\', '/');

            if (path[0] == '/')
                path = path.TrimStart(new[] { '/' });

            if (isDirectory && path.Length > 1 && path[path.Length - 1] != '/')
                path = path + '/';

            return path;
        }
    }
}